import re

# When sphinx (including the napoleon extension) parses the parameters
# section of a docstring, it converts the information into field lists.
# Some items in the list are for the parameter type. When the type fields
# are processed, the text is split and some tokens are turned into
# pending_xref nodes. These nodes are responsible for creating links.
#
# numpydoc does not create field lists, so the type information is
# not placed into fields that can be processed to make links. Instead,
# when parsing the type information we identify tokens that are link
# worthy and wrap them around a :obj: role.

# Note: we never split on commas that are not followed by a space
# You risk creating bad rst markup if you do so.

QUALIFIED_NAME_RE = re.compile(
    # e.g int, numpy.array, ~numpy.array, .class_in_current_module
    r'^'
    r'[~\.]?'
    r'[a-zA-Z_]\w*'
    r'(?:\.[a-zA-Z_]\w*)*'
    r'$'
)

CONTAINER_SPLIT_RE = re.compile(
    # splits dict(str, int) into
    #    ['dict', '[', 'str', ', ', 'int', ']', '']
    r'(\s*[\[\]\(\)\{\}]\s*|,\s+)'
)

CONTAINER_SPLIT_REJECT_RE = re.compile(
    # Leads to bad markup e.g.
    # {int}qualified_name
    r'[\]\)\}]\w'
)

DOUBLE_QUOTE_SPLIT_RE = re.compile(
    # splits 'callable ``f(x0, *args)`` or ``f(x0, y0, *args)``' into
    #    ['callable ', '``f(x0, *args)``', ' or ', '``f(x0, y0, *args)``', '']
    r'(``.+?``)'
)

ROLE_SPLIT_RE = re.compile(
    # splits to preserve ReST roles
    r'(:\w+:`.+?(?<!\\)`)'
)

SINGLE_QUOTE_SPLIT_RE = re.compile(
    # splits to preserve quoted expressions roles
    r'(`.+?`)'
)

TEXT_SPLIT_RE = re.compile(
    # splits on ' or ', ' | ', ', ' and ' '
    r'(\s+or\s+|\s+\|\s+|,\s+|\s+)'
)

CONTAINER_CHARS = set('[](){}')

# Save people some time and add some common standard aliases
DEFAULT_LINKS = {
    # Python
    'None': ':data:`python:None`',
    'bool': ':ref:`bool <python:bltin-boolean-values>`',
    'boolean': ':ref:`bool <python:bltin-boolean-values>`',
    'True': ':data:`python:True`',
    'False': ':data:`python:False`',
    'list': ':class:`python:list`',
    'tuple': ':class:`python:tuple`',
    'str': ':class:`python:str`',
    'string': ':class:`python:str`',
    'dict': ':class:`python:dict`',
    'float': ':class:`python:float`',
    'int': ':class:`python:int`',
    'callable': ':func:`python:callable`',
    'iterable': ':term:`python:iterable`',
    'sequence': ':term:`python:sequence`',
    'contextmanager': ':func:`python:contextlib.contextmanager`',
    'namedtuple': ':func:`python:collections.namedtuple`',
    'generator': ':term:`python:generator`',
    # NumPy
    'array': 'numpy.ndarray',
    'ndarray': 'numpy.ndarray',
    'np.ndarray': 'numpy.ndarray',
    'array-like': ':term:`numpy:array_like`',
    'array_like': ':term:`numpy:array_like`',
    'scalar': ':ref:`scalar <numpy:arrays.scalars>`',
    'RandomState': 'numpy.random.RandomState',
    'np.random.RandomState': 'numpy.random.RandomState',
    'np.inf': ':data:`numpy.inf`',
    'np.nan': ':data:`numpy.nan`',
    'numpy': ':mod:`numpy`',
}


def make_xref(param_type, xref_aliases, xref_ignore):
    """Parse and apply appropriate sphinx role(s) to `param_type`.

    The :obj: role is the default.

    Parameters
    ----------
    param_type : str
        text
    xref_aliases : dict
        Mapping used to resolve common abbreviations and aliases
        to fully qualified names that can be cross-referenced.
    xref_ignore : set
        Words not to cross-reference.

    Returns
    -------
    out : str
        Text with fully-qualified names and terms that may be wrapped in a
        ``:obj:`` role.
    """
    if param_type in xref_aliases:
        link, title = xref_aliases[param_type], param_type
        param_type = link
    else:
        link = title = param_type

    if QUALIFIED_NAME_RE.match(link) and link not in xref_ignore:
        if link != title:
            return ':obj:`%s <%s>`' % (title, link)
        else:
            return ':obj:`%s`' % link

    def _split_and_apply_re(s, pattern):
        """
        Split string using the regex pattern,
        apply main function to the parts that do not match the pattern,
        combine the results
        """
        results = []
        tokens = pattern.split(s)
        n = len(tokens)
        if n > 1:
            for i, tok in enumerate(tokens):
                if pattern.match(tok):
                    results.append(tok)
                else:
                    res = make_xref(
                        tok, xref_aliases, xref_ignore)
                    # Opening brackets immediately after a role is
                    # bad markup. Detect that and add backslash.
                    # :role:`type`( to :role:`type`\(
                    if res and res[-1] == '`' and i < n-1:
                        next_char = tokens[i+1][0]
                        if next_char in '([{':
                            res += '\\'
                    results.append(res)

            return ''.join(results)
        return s

    # The cases are dealt with in an order the prevents
    # conflict.
    # Then the strategy is:
    #   - Identify a pattern we are not interested in
    #   - split off the pattern
    #   - re-apply the function to the other parts
    #   - join the results with the pattern

    # Unsplittable literal
    if '``' in param_type:
        return _split_and_apply_re(param_type, DOUBLE_QUOTE_SPLIT_RE)

    # Any roles
    if ':`' in param_type:
        return _split_and_apply_re(param_type, ROLE_SPLIT_RE)

    # Any quoted expressions
    if '`' in param_type:
        return _split_and_apply_re(param_type, SINGLE_QUOTE_SPLIT_RE)

    # Any sort of bracket '[](){}'
    if any(c in CONTAINER_CHARS for c in param_type):
        if CONTAINER_SPLIT_REJECT_RE.search(param_type):
            return param_type
        return _split_and_apply_re(param_type, CONTAINER_SPLIT_RE)

    # Common splitter tokens
    return _split_and_apply_re(param_type, TEXT_SPLIT_RE)
